﻿#region 참조 네임스페이스

using System;
using System.Threading;
using Skying.ComponentModel;

#endregion

namespace Skying.Threading
{
	public sealed class SynchronizingTimer : IDisposable
	{
		#region 변수

		/// <summary>
		/// 메인 스레드에 대한 <see cref="SynchronizationContext"/>(동기화 컨텍스트)을 나타낸다.
		/// </summary>
		private SynchronizationContext _synchronizationContext;

		/// <summary>
		/// 스레드 기반의 타이머로 null일 경우 타이머가 멈춘 것을 뜻한다.
		/// </summary>
		private Timer _timer;

		private CallbackContext _context;

		/// <summary>
		/// <see cref="SynchronizingTimer"/> 클래스가 <see cref="Elapsed"/> 이벤트를 실행 중인지 아닌지를 나타낸다.
		/// </summary>
		private bool _isElapsing;

		/// <summary>
		/// <see cref="_isElapsing"/> 변수가 true일 때, <see cref="Enabled"/> 프로퍼티에 대한 backing 필드를 나타낸다.
		/// </summary>
		private bool _enabledAfterElapsed;

		/// <summary>
		/// <see cref="_isElapsing"/> 변수가 false일 때, <see cref="Interval"/> 프로퍼티에 대한 backing 필드를 나타낸다.
		/// </summary>
		private TimeSpan _interval;

		/// <summary>
		/// <see cref="_isElapsing"/> 변수가 true일 때, <see cref="Interval"/> 프로퍼티에 대한 backing 필드를 나타낸다.
		/// </summary>
		private TimeSpan _intervalAfterElapsed;

		#endregion

		#region 프로퍼티

		public bool Enabled
		{
			get
			{
				if (this._isElapsing)
				{
					return this._enabledAfterElapsed;
				}
				else
				{
					return this._timer != null;
				}
			}
			set
			{
				// If we are in the callback, just save the value and return (it will be applied after the callback returns)
				if (this._isElapsing)
				{
					this._enabledAfterElapsed = value;

					return;
				}

				// Do nothing if enabling an already-enabled timer, or disabling an already-disabled timer.
				if (this.Enabled == value)
				{
					return;
				}

				if (value)
				{
					// Start the timer

					// Bind the callback to our context and synchronization context
					Action boundOnTimer = this._context.AsyncBind(this.OnTimer, this._synchronizationContext, false);

					// The underlying timer delegate (raised on a ThreadPool thread) will first synchronize with the original thread
					//  using the captured SynchronizationContext. Then it will determine if its binding is still valid and call OnTimer
					//  if it's OK. OnTimer only handles the user callback logic.
					this._timer = new System.Threading.Timer((state) => boundOnTimer(), null, this._interval, TimeSpan.FromMilliseconds(-1));

					// Inform the synchronization context that there is an active asynchronous operation
					this._synchronizationContext.OperationStarted();
				}
				else
				{
					// Stop the underlying timer
					this._context.Reset();

					this._timer.Dispose();
					this._timer = null;

					// Inform the synchronization context that the asynchronous operation has completed
					this._synchronizationContext.OperationCompleted();
				}
			}
		}

		public bool AutoReset
		{
			get;
			set;
		}

		public TimeSpan Interval
		{
			get
			{
				if (this._isElapsing)
				{
					return this._intervalAfterElapsed;
				}
				else
				{
					return this._interval;
				}
			}
			set
			{
				// If we are in the callback, just save the value and return (it will be applied after the callback returns)
				if (this._isElapsing)
				{
					this._intervalAfterElapsed = value;
					return;
				}

				// If the timer is already running, then stop it, set the time, and restart it.
				if (this.Enabled)
				{
					this.Enabled = false;

					this._interval = value;

					this.Enabled = true;

					return;
				}

				// The timer is not already running, so we can just directly set the value.
				this._interval = value;
			}
		}

		#endregion

		#region 이벤트

		public event Action Elapsed;

		#endregion

		#region 생성자/소멸자

		public SynchronizingTimer()
		{
			// 메인 스레드에 동기화 컨텍스트를 가져온다.
			this._synchronizationContext = SynchronizationContext.Current;

			if (this._synchronizationContext == null)
			{
				this._synchronizationContext = new SynchronizationContext();
			}

			// 동기화 컨텍스트가 동기화되어져 있는지를 확인한다.
			SynchronizationContextRegister.Verify(this._synchronizationContext.GetType(), SynchronizationContextProperties.Synchronized);

			this._context = new CallbackContext();
		}

		#endregion

		public void SetSingleShot(TimeSpan interval)
		{
			// Only public properties are used here to allow this function to be called from Elapsed
			this.Enabled = false;
			this.AutoReset = false;
			this.Interval = interval;
			this.Enabled = true;
		}

		public void SetPeriodic(TimeSpan period)
		{
			// Only public properties are used here to allow this function to be called from Elapsed
			this.Enabled = false;
			this.AutoReset = true;
			this.Interval = period;
			this.Enabled = true;
		}

		public void Cancel()
		{
			// Only public properties are used here to allow this function to be called from Elapsed
			this.Enabled = false;
		}

		public void Dispose()
		{
			this.Enabled = false;
			this._context.Dispose();
		}

		public void Restart()
		{
			this.Enabled = false;
			this.Enabled = true;
		}

		private void OnTimer()
		{
			// When this is called, we have already been synchronized with the original thread and our context is valid (see Enabled.set() for details).

			// Copy properties for use from within the callback; these are the "default values"
			this._enabledAfterElapsed = this.AutoReset;
			this._intervalAfterElapsed = this._interval;

			// Set the flag indicating we're in the callback
			this._isElapsing = true;

			// Call the callback
			try
			{
				if (this.Elapsed != null)
				{
					this.Elapsed();
				}
			}
			finally
			{
				// Reset "in callback" flag
				this._isElapsing = false;

				// Apply all variables that may have been set in the callback
				this._interval = this._intervalAfterElapsed;
				if (!this._enabledAfterElapsed)
				{
					// Destroy the underlying timer
					this._timer.Dispose();
					this._timer = null;
				}
				else
				{
					// Since the timer is enabled (either single-shot or periodic, we don't care), and since it has already elapsed, we can just
					//  re-use the underlying timer object and context instead of re-creating them at this point.
					this._timer.Change(this._interval, TimeSpan.FromMilliseconds(-1));
				}
			}
		}
	}
}